package com.ihome.framework.core.id.service;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Resource;

import org.apache.commons.lang.builder.HashCodeBuilder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.Assert;

import com.fasterxml.uuid.EthernetAddress;
import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.TimeBasedGenerator;
import com.ihome.framework.core.cache.nkv.IHomeNkvClient;
import com.ihome.framework.core.id.GeneratorException;
import com.ihome.framework.core.id.dao.IBasicBizSeqIdDao;
import com.ihome.framework.core.id.domain.SysSequence;
import com.netease.backend.nkv.client.NkvClient.NkvOption;

/**
 * 
 * @author
 *
 */
public class IdGeneratorService implements InitializingBean, DisposableBean, ApplicationContextAware {

    private final IdWorker ID_WORKER = new IdWorker();
    @Resource
    private IHomeNkvClient nkvClient;
    @Resource
    private IBasicBizSeqIdDao bizSeqIdDao;

    /** static **/
    private static IdGeneratorService generator;
    private static DataSourceTransactionManager transactionManager;
    private static final ConcurrentHashMap<String, IncrBatchAdapter> batchAdapters = new ConcurrentHashMap<String, IdGeneratorService.IncrBatchAdapter>();
    private static final char[] HEX_CHARS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
            'c', 'd', 'e', 'f' };
    private static final Integer DEFAULT_BATCH_SIZE = 100;
    private static final Integer ONE_BATCH_SIZE = 1;
    private static final Integer ONE_STEP_SIZE = 1;


    public static final Integer LEN_20 = 20;
    public static final Integer LEN_22 = 22;
    public static final Integer LEN_24 = 24;
    public static final Integer LEN_25 = 25;



    public static String generateChars(String prefix, long seq) {
        return generateChars(prefix, seq, -1);
    }


    public static String generateChars(String prefix, long seq,int charsLength) {
        Assert.hasLength(prefix);
        long time = (long) (System.currentTimeMillis());
        byte[] bytes = new byte[8];
        char[] chars = new char[bytes.length * 2];
        bytes[0] = (byte) (time >> 24);
        bytes[1] = (byte) (time >> 16);
        bytes[2] = (byte) (time >> 8);
        bytes[3] = (byte) (time);
        bytes[4] = (byte) (seq >> 24);
        bytes[5] = (byte) (seq >> 16);
        bytes[6] = (byte) (seq >> 8);
        bytes[7] = (byte) (seq);
        int i = 0;
        for (byte b : bytes) {
            chars[i++] = HEX_CHARS[b >> 4 & 0xF];
            chars[i++] = HEX_CHARS[b & 0xF];
        }
        StringBuffer buffer = new StringBuffer(prefix).append(chars);

        int complementLength = 1;  // Least Complement Length
        if(buffer.length() < charsLength)
        {
            // Complement Char Length
            complementLength = charsLength - buffer.length();
        }

        for (int j = 0; j < complementLength; j++) {
            buffer.append(ThreadLocalRandom.current().nextInt(0, 10));
        }
        return buffer.toString();
    }




    //by test
    static void clearBatchAdapter() {
        batchAdapters.clear();
    }

    public static String generateChars(String prefix, String tableName) {
        return generateChars(prefix,tableName,-1);
    }

    public static String generateChars(String prefix, String tableName,int charsLength) {
        long seq = 0l;
        try {
            seq = autoIncrBatchNkv(prefix, tableName, 1, 0);
        } catch (Exception e) {
            try {
                seq = generateBigLong();
            } catch (Exception e1) {
                throw new GeneratorException("incr seq error", e1);
            }
        }
        // call base generate char method
        return generateChars(prefix, seq, charsLength);
    }

    public static long generateBigLong() throws RuntimeException {
        return generator.ID_WORKER.nextId();
    }

    public static long autoIncrNkv(String prefix, String tableName, int stepValue,int initValue) throws RuntimeException {
        return autoIncr(new NkvIncrBatchAdapter(prefix, tableName, initValue, ONE_BATCH_SIZE, stepValue), prefix, tableName);
    }

    public static long autoIncrNkv(String prefix, String tableName, int initValue) throws RuntimeException {
        return autoIncrNkv(prefix, tableName, ONE_STEP_SIZE, initValue);
    }

    public static long autoIncrBatchNkv(String prefix, String tableName, int stepValue, int initValue)
            throws RuntimeException {
        return autoIncr(new NkvIncrBatchAdapter(prefix, tableName, initValue, DEFAULT_BATCH_SIZE, stepValue), prefix, tableName);
    }

    public static long autoIncrBatchRds(String prefix, String tableName, int stepValue, int initValue) throws RuntimeException {
        return autoIncr(new RdsIncrBatchAdapter(prefix, tableName, initValue, DEFAULT_BATCH_SIZE, stepValue), prefix, tableName);
    }

    public static long autoIncrBatchNkv(String tableName, int stepValue, int initValue) throws RuntimeException {
        return autoIncrBatchNkv("", tableName, stepValue, initValue);
    }

    public static long autoIncrBatchRds(String tableName, int stepValue, int initValue) throws RuntimeException {
        return autoIncrBatchRds("", tableName, stepValue, initValue);
    }

    /**
     * 
     * @param prefix
     *            前缀
     * @param tableName
     *            表名
     * @param stepValue
     *            步长
     * @param initValue
     *            初始值
     * @return
     * @throws Exception
     */
    public static long autoIncr(IncrBatchAdapter adapter, String prefix, String tableName) throws RuntimeException {
        synchronized (DEFAULT_BATCH_SIZE) {
            String key = adapter.buildSeqName(prefix, tableName);
            if (!batchAdapters.containsKey(key)) {
                batchAdapters.putIfAbsent(key, adapter);
            }
            return batchAdapters.get(key).autoIncr();
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        generator = this;
        generator.nkvClient = nkvClient;
        generator.bizSeqIdDao = bizSeqIdDao;
    }

    @Override
    public void destroy() throws Exception {
        for (String key : batchAdapters.keySet()) {
            batchAdapters.get(key).destroy();
        }
        batchAdapters.clear();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        transactionManager = (DataSourceTransactionManager) applicationContext
                .getBean(PlatformTransactionManager.class);
    }

    public static class RdsIncrBatchAdapter extends IncrBatchAdapter {

        public RdsIncrBatchAdapter(String prefix, String tableName, int initValue, Integer batchSize,
                Integer stepSize) {
            super(prefix, tableName, initValue, batchSize, stepSize);
        }

        @Override
        protected long batchApplyIncr(String prefix, String tableName, int initValue) {
            // open transaction
            long applySize = 0L;
            TransactionStatus status = openTransaction();
            try {
                applySize = generator.bizSeqIdDao.nextval(seqName);
                commit(status);
            } catch (Exception e) {
                rollback(status);
                throw e;
            }
            return applySize;

        }

        protected TransactionStatus openTransaction() {
            DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
            definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
            return transactionManager.getTransaction(definition);
        }

        protected void commit(TransactionStatus status) {
            status.flush();
            transactionManager.commit(status);
        }

        protected void rollback(TransactionStatus status) {
            transactionManager.rollback(status);
        }

        @Override
        protected long startIncr(String prefix, String tableName, int initValue) {
            return generator.bizSeqIdDao.currval(seqName);
        }

        @Override
        public void afterPropertiesSet() throws Exception {
            super.afterPropertiesSet();
            // check
            Assert.notNull(transactionManager); // transaction manager not null
            long currVal = startIncr(prefix, tableName, initValue);
            if (currVal < 0L) {
                TransactionStatus status = openTransaction();
                try {
                    generator.bizSeqIdDao.insert(new SysSequence(seqName, initValue, stepSize * batchSize)); // insert config
                    commit(status);
                } catch (Exception e) {
                    rollback(status);
                    throw e;
                }
            }
        }

    }

    public static class NkvIncrBatchAdapter extends IncrBatchAdapter {

        public NkvIncrBatchAdapter(String prefix, String tableName, int initValue, int batchSize, int stepSize) {
            super(prefix, tableName, initValue, batchSize, stepSize);
        }

        @Override
        protected long batchApplyIncr(String prefix, String tableName, int initValue) {
            return generator.nkvClient.incr(prefix, tableName, batchSize * stepSize, initValue,
                    new NkvOption(60 * 60 * 24));
        }

        @Override
        protected long startIncr(String prefix, String tableName, int initValue) {
            throw new UnsupportedOperationException();
        }

    }

    public static abstract class IncrBatchAdapter implements InitializingBean, DisposableBean {
        private volatile long sequence = 0L; // current
        private volatile long lastIncr;
        private volatile long startIncr;
        private AtomicBoolean isInited = new AtomicBoolean(false);

        /** init params **/
        protected final Integer batchSize;
        protected final Integer stepSize;
        protected final String seqName;
        protected final int initValue;
        protected final String prefix;
        protected final String tableName;

        public IncrBatchAdapter(String prefix, String tableName, int initValue, int batchSize, int stepSize) {
            this.batchSize = batchSize <= 0 ? 1 : batchSize;
            this.stepSize = stepSize;
            this.seqName = buildSeqName(prefix, tableName);
            this.initValue = initValue;
            this.prefix = prefix;
            this.tableName = tableName;
        }

        public long autoIncr() {
            synchronized (IdGeneratorService.DEFAULT_BATCH_SIZE) {
                try {
                    if (isInited.compareAndSet(false, true)) {
                        try {
                            afterPropertiesSet();
                        } catch (Exception e) {
                            throw new GeneratorException("init Incr batch adapter error", e);
                        }
                    }
                    if (sequence > lastIncr || lastIncr == 0L) {
                        lastIncr = batchApplyIncr(prefix, tableName, initValue);
                        startIncr = lastIncr - ((batchSize - 1) * stepSize);
                        sequence = startIncr;
                    }
                    return sequence;
                } finally {
                    sequence = sequence + stepSize;
                }
            }
        }

        protected String buildSeqName(String prefix, String tableName) {
            return new StringBuffer(prefix).append(tableName).toString();
        }

        @Override
        public void afterPropertiesSet() throws Exception {
        }

        @Override
        public void destroy() throws Exception {
        }

        @Override
        public int hashCode() {
            return HashCodeBuilder.reflectionHashCode(this);
        }

        protected abstract long batchApplyIncr(String prefix, String tableName, int initValue);

        protected abstract long startIncr(String prefix, String tableName, int initValue);

    }

    public static class UUIDWorker{
        private static TimeBasedGenerator timeBasedGenerator = Generators.timeBasedGenerator(EthernetAddress.fromInterface());
        public static String getNextId() {
            return timeBasedGenerator.generate().toString().replaceAll("-", "");
        }
    }

    public static class IdWorker {

        private final long twepoch = 1288834974657L;  
        private final long workerIdBits = 10L;  
        private final long datacenterIdBits = 5L;  
        private final long maxWorkerId = -1L ^ (-1L << workerIdBits);  // max worker id by 1023
        private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // max data center id by 31
        private final long sequenceBits = 12L;  
        private final long workerIdShift = sequenceBits;  
        private final long datacenterIdShift = sequenceBits + workerIdBits;  
        private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;  
        private final long sequenceMask = -1L ^ (-1L << sequenceBits);  

        private long workerId;  
        private long datacenterId;  
        private long sequence = 0L;  
        private long lastTimestamp = -1L;  

        public IdWorker(){
            this(ThreadLocalRandom.current().nextLong(1, 999),ThreadLocalRandom.current().nextLong(1, 25));
        }


        public IdWorker(long workerId, long datacenterId) {  
            if (workerId > maxWorkerId || workerId < 0) {  
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));  
            }  
            if (datacenterId > maxDatacenterId || datacenterId < 0) {  
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));  
            }  
            this.workerId = workerId;  
            this.datacenterId = datacenterId;  
        }  

        public synchronized long nextId() {  
            long timestamp = timeGen();  
            if (timestamp < lastTimestamp) {  
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));  
            }  
            if (lastTimestamp == timestamp) {  
                sequence = (sequence + 1) & sequenceMask;  
                if (sequence == 0) {  
                    timestamp = tilNextMillis(lastTimestamp);  
                }  
            } else {  
                sequence = 0L;  
            }  

            lastTimestamp = timestamp;  
            return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;  
        }  

        protected long tilNextMillis(long lastTimestamp) {  
            long timestamp = timeGen();  
            while (timestamp <= lastTimestamp) {  
                timestamp = timeGen();  
            }  
            return timestamp;  
        }  

        protected long timeGen() {  
            return System.currentTimeMillis();  
        }  

    }
}
